Detect wedged vGPU VFs from the guest and report to the health store - #435
Detect wedged vGPU VFs from the guest and report to the health store#435yummybomb wants to merge 2 commits into
Conversation
d312338 to
7d54fb9
Compare
7d54fb9 to
9a90223
Compare
9a90223 to
0ff16d1
Compare
0ff16d1 to
3db5546
Compare
3db5546 to
b35501a
Compare
b3bf925 to
55a0d6f
Compare
55a0d6f to
ee1b160
Compare
a5dc229 to
fed19c4
Compare
7bd4f52 to
67b2b24
Compare
67b2b24 to
0662ad9
Compare
f3bf0cc to
ff54c20
Compare
ff54c20 to
37bc190
Compare
yummybomb
left a comment
There was a problem hiding this comment.
one blocking test-quality issue. the sentinel/store boundaries, fail-closed placement behavior, assignment revalidation, and persistence rollback paths otherwise look coherent in context. targeted race tests and go vet pass locally; the current linux CI also exposes the changed-test failure below.
| require.NoError(t, os.WriteFile(nvidiaSMI, []byte(script), 0o755)) | ||
| t.Setenv("COUNT_PATH", countPath) | ||
|
|
||
| probeGPUInitUntil(&gpuInitReporter{}, nvidiaSMI, time.Now().Add(time.Second), 10*time.Millisecond, 0) |
There was a problem hiding this comment.
blocker: this 10ms deadline is shorter than normal shell startup under race/loaded CI, so the nominally successful second attempt can time out too. The current Linux job failed here with actual: 3, and go test -race ... -count=100 reproduces it locally. Please make the attempt runner/clock injectable and use a deterministic fake, or otherwise give the success attempt independent scheduling margin; asserting exactly two real process launches with this deadline is flaky.
There was a problem hiding this comment.
fixed in 88f3c3d by injecting the probe attempt into the retry loop and testing the timeout/success sequence directly. go test -race ./lib/system/guest_agent -run TestProbeGPUInitRetriesAfterAttemptTimeout -count=100, the package race suite, and go vet pass.
yummybomb
left a comment
There was a problem hiding this comment.
two code-quality findings. the store/placement boundaries, persistence rollback, assignment revalidation, and tests otherwise look coherent in repo context. the new functions peak at cyclomatic complexity 16 (loadLocked / selectLeastLoadedVF); those paths remain cohesive and well-covered, so I don't see mechanical decomposition as a merge requirement.
| func (c *VGPUSentinelController) scanTarget(ctx context.Context, target vgpuSentinelTarget) { | ||
| tail := c.tails[target.instanceID] | ||
| if tail == nil || tail.vfAddress != target.vfAddress || tail.assignedAt != target.assignedAt { | ||
| tail = &vgpuSentinelTail{vfAddress: target.vfAddress, assignedAt: target.assignedAt} |
There was a problem hiding this comment.
The tail offset is process-local, but a successful report removes the only persistent dedup record. After FAILED -> OK, restarting hypeman starts at offset 0, records the historical failure again, increments init_failures_total, then clears it again on the historical OK. With vf_quarantine_threshold: 1, replay also emits a fresh quarantine/rescind and increments both counters on every restart; if that replayed clear fails, a healthy VF stays quarantined. Please retain a bounded last-resolved assignment/watermark per VF (or coalesce a replay before mutating state) and add a restart regression test.
| } | ||
|
|
||
| reader := bufio.NewReaderSize(f, vgpuSentinelMaxLineBytes) | ||
| for { |
There was a problem hiding this comment.
non-blocking: please thread ctx into scanSentinelLog and check cancellation between reads (and target scans). On the first pass after controller/process start every assignment begins at byte 0, so a host with many logs near the 50 MB rotation limit can keep Run inside this loop and hold the process-level errgroup.Wait well after shutdown was requested.
yummybomb
left a comment
There was a problem hiding this comment.
reviewed the change against hypeship/vendor-vfio-vgpu and traced the sentinel through guest output, host log scanning, durable health state, placement, and admission. the persistence rollback and assignment-identity handling are coherent, but the serial marker is not actually source-authenticated, and timed-out probes can overlap in the exact uninterruptible-ioctl case this code targets.
leaving the first finding as a blocker; github does not allow this account to request changes on its own PR.
|
|
||
| // Require a standalone guest-agent line so echoed marker text cannot count against a VF. | ||
| var ( | ||
| vgpuSentinelFailedPattern = regexp.MustCompile(`^\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} \[guest-agent\] (HYPEMAN-GPU-INIT-FAILED ts=\S+ nvrm="NVRM: [^"\r\n]*RmInitAdapter failed![^"\r\n]*")\r?\n?$`) |
There was a problem hiding this comment.
blocker: a standalone line does not establish that the guest agent wrote it. In exec mode the customer entrypoint's stdout/stderr is wired directly to the same serial console (lib/system/init/mode_exec.go:132-133), so an ordinary workload can print a complete line matching this regex without root or /dev/kmsg access. Repeating that across assignments can quarantine healthy VFs, and the runbook's statement that only a root guest can forge this is therefore incorrect. Please carry the report over a channel whose source the host can distinguish (for example a dedicated guest-agent RPC/transport), or otherwise authenticate the source; matching the log shape is not sufficient here.
| case err := <-done: | ||
| return err | ||
| case <-timer.C: | ||
| _ = cmd.Process.Kill() |
There was a problem hiding this comment.
this bounds how long the caller waits, but not the attempt's lifetime. If nvidia-smi is stuck in the uninterruptible ioctl described above, Kill does not make cmd.Wait return; the process and waiter goroutine remain live while probeGPUInitUntil launches another attempt after 15s. The 10-minute loop can therefore accumulate many concurrent stuck probes on the VF. Can we retain the in-flight attempt and avoid launching another until it is reaped (or stop retrying once an attempt cannot be killed), so there is at most one outstanding nvidia-smi?
88f3c3d to
b901282
Compare
b901282 to
1fd824a
Compare
a805d59 to
aa66cd3
Compare
1084aef to
b23a654
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit b23a654. Configure here.
b49bc73 to
a53ca5c
Compare
a53ca5c to
86fb40e
Compare
The guest agent watches /dev/kmsg for kernel-facility NVRM RmInitAdapter failures, probes driver init at boot with nvidia-smi -L when present, and exposes the current GPU init state over GetGPUInitStatus via vsock. A host sentinel controller polls every vendor VFIO instance with a live VMM (control socket present) at bounded concurrency and reports failures and successes into the VF health store, which quarantines a VF once failures accumulate from enough distinct assignments. Reports are keyed on GPUClaimedAt, the identity a claim receives when it is persisted, so rescans and controller restarts cannot double-count an assignment. Reconcile now fails closed on an ambiguous hypervisor ownership check instead of treating it as a plain boolean: the claim is preserved, a warning names the instance and device, and hypeman_instances_vgpu_reconcile_liveness_uncertain_total counts it. The sentinel repairs a failed health store load or persist once per poll so reports do not retry it inline.
86fb40e to
322c599
Compare
Build the sentinel's vsock dialer from the metadata already loaded for the poll target instead of GetInstance, which derived full instance state and could persist exit info or boot markers on every 5s tick. Route the create/start cleanup guard through the same liveness wrapper as reconcile so an uncertain hypervisor check logs and counts once; the counter is renamed hypeman_instances_vgpu_liveness_uncertain_total. Move the quarantined-VF and health-store gauges next to the other instance metrics, and drop the nil check on a controller wire cannot leave nil. Share one /dev/kmsg record reader between the GPU init watcher and the clock keeper, log each failed nvidia-smi probe attempt with its output, and report the actual retry window when the probe gives up.

Summary
Top half of the wedged-VF work, stacked on #462 (the VF health store, placement exclusion, admission, and
/resourcesfields). This PR adds the detection path that feeds that store./dev/kmsgfor kernel-facilityNVRM: ... RmInitAdapter failed!records, probes driver init at boot withnvidia-smi -Lwhen the image has it, and exposes the current GPU initialization state overGetGPUInitStatusvia vsock.GPUClaimedAtfrom Quarantine unhealthy vGPU VFs via a persisted health store #462, the identity a claim receives when it is persisted.Rebuilt on the claim-first allocator
This branch was rebuilt from the new #462 head after #321 merged as the claim-first rewrite. The guest agent, guest RPC, sentinel controller, wiring, metrics, and docs are the previously reviewed code, squashed into one commit. What changed in the port:
GPUAssignedAtis gone frommain; the sentinel keys reports onGPUClaimedAt, which Quarantine unhealthy vGPU VFs via a persisted health store #462 sets in the same metadata save as the claim.GPURetainedForCleanupskip is gone with the retention-record design. The existing control-socket check already excludes instances without a VMM.create.gois dropped. Claim-first already persists the claim before the VF is touched.main's twohypervisorMayBeAlivecall sites; reconcile and the create/start cleanup guard share one wrapper that logs the resolution error and increments the liveness counter when it fails closed.Safety and failure handling
nvidia-smiattempt is killed after 30 seconds. The guest agent waits for that process to be reaped before retrying, so an attempt stuck in uninterruptible I/O cannot accumulate concurrent probes; the independent kmsg watcher still reports the underlying init failure.Observability
hypeman_instances_vgpu_sentinel_init_failures_totalhypeman_instances_vgpu_sentinel_quarantines_totalhypeman_instances_vgpu_sentinel_checks_totalbyresult(ok,failed,unknown,rpc_error,unsupported_agent, orlist_error)hypeman_instances_vgpu_quarantined_vfshypeman_instances_vgpu_vf_health_store_unavailablehypeman_instances_vgpu_liveness_uncertain_totallib/devices/GPU.mdgains the detection and sentinel documentation.Out of scope
An operator force-cycle endpoint. Recovery remains the documented manual DCGM quiesce, SR-IOV cycle, state edit, restart, and verification flow.
Testing
Passed locally:
cmd/api/wire_gen.gowas regenerated withwireand matches.lib/guest/guest.pb.goandguest_grpc.pb.goare carried over from the previous head;guest.protohas not changed onmainsince, and the header records the sameprotocandprotoc-gen-goversions as the committed files.The full
lib/instancessuite was not run here; tests that boot real VM images need a host this environment does not provide.The underlying wedge signal and manual recovery sequence were previously validated on L40S hardware. A live end-to-end run of the guest watcher and host controller is still required before merge, including two failed assignments and the success path.